﻿// ===============================================================================
//  Copyright © ymind.net. All rights reserved .
//  官方网站：http://ymind.net/
//  版权所有：彦铭工作室 老陈出品
// ===============================================================================

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using Lcsoft.Data.Reflection;

namespace Lcsoft.Data.RDBMS
{
	/// <summary>
	/// 表示用于填充实体对象集合和更新数据库的一组数据命令和一个数据库连接。无法继承此类。
	/// </summary>
	[ComVisible(true)]
	public static class ObjectSqlAdapter
	{
		private const string _OUTPUT_PREFIX = "HYDRATE_";
		private static readonly LazyInitCache<string, ConstructorInfo> _constructorCache = new LazyInitCache<string, ConstructorInfo>();
		private static readonly LazyInitCache<string, Func<DbDataReader, object>> _objectMappingCache = new LazyInitCache<string, Func<DbDataReader, object>>();
		private static readonly LazyInitCache<string, PropertyInfo[]> _propertyCache = new LazyInitCache<string, PropertyInfo[]>();

		private static string _StripParamPrefix(string paramName) { return paramName.Replace("@", String.Empty).Replace(":", String.Empty).Replace("?", String.Empty); }

		private static string _GetObjectCachingKey<T>(string prefix)
		{
			var properties = _GetAllProperties<T>();
			var objectKey = new StringBuilder(prefix);

			foreach (var property in properties) objectKey.Append(property.Name);

			return objectKey.ToString();
		}

		private static Func<DbDataReader, object> _MapObject<T>(IDataRecord reader)
		{
			var mappedPropertyDelegates = _MapProperties<T>(reader);

			return rdr =>
			{
				object obj = _CreateInstance<T>();

				foreach (var propDelegate in mappedPropertyDelegates) propDelegate.Invoke(rdr, obj);

				return obj;
			};
		}

		private static T _CreateInstance<T>()
		{
			var instance = DynamicMethodCompiler.CreateInstance<T>(_GetDefaultConstructor<T>());

			return instance != null ? (T)instance.Invoke() : default(T);
		}

		private static ConstructorInfo _GetDefaultConstructor<T>()
		{
			var t = typeof(T);

			return _constructorCache.Fetch(t.FullName, () => t.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[0], null));
		}

		private static IEnumerable<Action<DbDataReader, object>> _MapProperties<T>(IDataRecord reader)
		{
			var properties = _GetAllProperties<T>();
			var propertyMappings = new List<Action<DbDataReader, object>>();

			if (properties == null || properties.Length == 0) return propertyMappings;

			for (var i = 0; i < reader.FieldCount; i++)
			{
				var columnName = reader.GetName(i);

				var matchingProperty = properties.FirstOrDefault(p =>
				{
					var propColumnAttr = p.GetCustomAttributes(typeof(ColumnAttribute), false).FirstOrDefault() as ColumnAttribute;
					var propertyName = (propColumnAttr == null) ? p.Name : propColumnAttr.Name;

					return String.Compare(columnName, propertyName, StringComparison.InvariantCultureIgnoreCase) == 0;
				});

				if (matchingProperty == null) continue;

				propertyMappings.Add(_CreateValueSetter<T>(matchingProperty, columnName));
			}

			return propertyMappings;
		}

		private static Action<DbDataReader, object> _CreateValueSetter<T>(PropertyInfo property, string columnName)
		{
			return ((iReader, iObject) =>
			{
				if (Convert.IsDBNull(iReader[columnName])) return;

				var propertyType = property.PropertyType;
				var setHandle = DynamicMethodCompiler.CreateSetPropertyHandler<T>(property);

				var setAction = propertyType.BaseType == typeof(Enum) ? (Action<DbDataReader, object>)((reader, obj) =>
				{
					var objValue = _IsNaN(reader[columnName]) ? reader[columnName] : Convert.ChangeType(reader[columnName], propertyType);
					var value = Enum.ToObject(propertyType, objValue);

					setHandle.Invoke(obj, value);
				}) : ((reader, obj) => setHandle.Invoke(obj, Convert.ChangeType(reader[columnName], propertyType)));

				setAction.Invoke(iReader, iObject);
			});
		}

		private static PropertyInfo[] _GetAllProperties<T>()
		{
			var objType = typeof(T);

			return _propertyCache.Fetch(objType.FullName, objType.GetProperties);
		}

		private static bool _IsNaN(object value)
		{
			double outNum;

			return !Double.TryParse(Convert.ToString(value), NumberStyles.Any, NumberFormatInfo.InvariantInfo, out outNum);
		}

		/// <summary>
		/// 将 <paramref name="reader"/> 中的数据合成到给定类型的实例当中。
		/// </summary>
		/// <typeparam name="T">指定缓存对象的类型。</typeparam>
		/// <param name="reader">从数据源读取行的一个只进流。</param>
		/// <returns>返回包含了 <paramref name="reader"/> 数据的 <typeparamref name="T"/> 的实例。</returns>
		public static T HydrateObject<T>(DbDataReader reader)
		{
			var key = _GetObjectCachingKey<T>(_OUTPUT_PREFIX);

			return (T)_objectMappingCache.Fetch(key, () => _MapObject<T>(reader)).Invoke(reader);
		}

		/// <summary>
		/// 将指定类型的对象中的数据分离到 <see cref="System.Data.Common.DbCommand"/>，并将 <see cref="System.Data.Common.DbCommand"/> 作为参数封装到 Action 委托。
		/// </summary>
		/// <typeparam name="T">指定缓存对象的类型。</typeparam>
		/// <param name="obj">指定包含要分离的数据的对象的实例。</param>
		/// <returns>返回一个参数为 <see cref="System.Data.Common.DbCommand"/> 的 Action 委托。</returns>
		public static Action<DbCommand> DeHydrateObject<T>(object obj)
		{
			var properties = obj.GetType().GetProperties();

			return cmd =>
			{
				foreach (DbParameter parameter in cmd.Parameters)
				{
					if (parameter.Direction == ParameterDirection.Output) continue;

					var dbParameter = parameter;
					var currentProp = properties.FirstOrDefault(p =>
					{
						var paramName = _StripParamPrefix(dbParameter.ParameterName);

						return String.Compare(p.Name, paramName, StringComparison.InvariantCultureIgnoreCase) == 0;
					});

					if (currentProp == null) continue;

					var handler = DynamicMethodCompiler.CreatePropertyGetHandler<T>(currentProp);

					cmd.Parameters[parameter.ParameterName].Value = handler.Invoke(obj);
				}
			};
		}
	}
}
